package Logic;

import Core.Log4j2;
import Core.NetData;
import Core.ProtoCenter;
import com.mongodb.MongoClient;
import com.mongodb.client.MongoCollection;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import microgame.airplane.proto.Game;
import microgame.airplane.proto.MsgTypeOuterClass;
import microgame.airplane.proto.ProtoPacker;
import microgame.toolset.database.DatabaseControl;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonValue;
import org.bson.Document;
import org.bson.conversions.Bson;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Updates;

import java.util.Hashtable;
import java.util.Map;
import java.util.Stack;

import static com.mongodb.client.model.Filters.eq;

public class MatchManager implements ProtoProcessorInterface {
    private static MatchManager instance;
    private Hashtable<Integer, Match> matchMap = new Hashtable<>();
    private static int currentId = 0;
    private Stack<Integer> matchIdStack = new Stack<>();

    public static MatchManager Instance() {
        if (instance == null) {
            instance = new MatchManager();
        }
        return instance;
    }

    public void Init() {
        RegisterProtoProcessor();
    }

    public Match CreateMatch(int capcity) {
        if (matchIdStack.empty()) {
            return new Match(currentId++, capcity);
        }
        else {
            return new Match(matchIdStack.pop(), capcity);
        }
    }

    public Match SearchJoinableMatch(int capcity) {
        for (Map.Entry<Integer, Match> entry : matchMap.entrySet()) {
            if (entry.getValue().IsJoinable(capcity)) {
                return entry.getValue();
            }
        }

        return null;
    }

    public Match GetMatch(int id) {
        return matchMap.get(id);
    }

    public void RemoveMath(int id) {
        matchMap.remove(id);
    }


    public void RemoveMatch(int id) {
        matchMap.remove(id);
    }

    public void Update() {
        for (Map.Entry<Integer, Match> entry : matchMap.entrySet()) {
            boolean running = entry.getValue().Update();
            if (!running) {
                int id = entry.getValue().GetId();
                RemoveMatch(id);
                matchIdStack.push(id);
            }
        }
    }

    private boolean CheckClientInMatch(String clientId) {
        for (Map.Entry<Integer, Match> entry : matchMap.entrySet()) {
            if (entry.getValue().Contains(clientId)) return true;
        }
        return false;
    }

    private void CancelJoinMatch(NetData nd) {
        String clientId = nd.GetClientId();
        for (Map.Entry<Integer, Match> entry : matchMap.entrySet()) {
            if (entry.getValue().Contains(clientId)) {
                entry.getValue().RemoveClientFromId(clientId);

                Game.sCancelMatchAck.Builder builder = Game.sCancelMatchAck.newBuilder();
                builder.setCode(200);
                Game.sCancelMatchAck ack = builder.build();
                Util.SendMessageThruChannel(MsgTypeOuterClass.MsgType.sCancelMatchAck.getNumber(), nd.GetChannel(), ack.toByteArray());
                return;
            }
        }
    }

    private void ProtoStartMatchReq(NetData nd) {
        if (!Util.CheckLogon(nd)) return;

        try {
            Game.cStartMatchReq smReq = Game.cStartMatchReq.parseFrom(nd.GetData());

            Match match = SearchJoinableMatch(smReq.getCapcity());
            if (match == null) {
                match = CreateMatch(smReq.getCapcity());
                matchMap.put(match.GetId(), match);
            }

            match.AddClientFromId(nd.GetClientId());

            Game.sStartMatchAck.Builder smBuilder = Game.sStartMatchAck.newBuilder();
            smBuilder.setMatchid(match.GetId());
            Game.sStartMatchAck ack = smBuilder.build();
            Util.SendMessageThruChannel(MsgTypeOuterClass.MsgType.sStartMatchAck.getNumber(), nd.GetChannel(), ack.toByteArray());
        } catch (Exception e) {
            Log4j2.LOGGER.error(e.toString());
        }
    }

    private void ProtoCancelMatchReq(NetData nd) {
        if (!Util.CheckLogon(nd)) return;

        if (CheckClientInMatch(nd.GetClientId())) {
            CancelJoinMatch(nd);
        }
    }

    private void ProtoPropStubCollisionReq(NetData nd) {
        if (!Util.CheckLogon(nd)) return;

        try {
            Game.cPropStubCollisionReq req = Game.cPropStubCollisionReq.parseFrom(nd.GetData());
            int clientid = req.getPlayerid();
            int propid = req.getId();
            int matchid = req.getMatchid();
            int templateId = req.getTemplateid();
            Match match = GetMatch(matchid);
            if (match.Contains(clientid)) {
                match.GetPropStubManager().PropStubCollision(clientid, templateId, propid);
            }
        } catch (Exception e) {
            Log4j2.LOGGER.error(e.toString());
        }
    }

    private void ProtoPropCollidePlayerReq(NetData nd) {
        if (!Util.CheckLogon(nd)) return;

        try {
            //这里的设计思路是，客户端发现碰撞，并不马上产生效果，必须等待服务器的push到后才出效果，加分等
            Game.cPropCollidePlayerReq req = Game.cPropCollidePlayerReq.parseFrom(nd.GetData());
            int propId = req.getPropid();
            int playerId = req.getPlayerid();
            int playerIdCollide = req.getPlayeridcollide();
            int matchId = req.getMatchid();
            int templateId = req.getTemplateid();

            Game.sPropCollidePlayerPush.Builder builder = Game.sPropCollidePlayerPush.newBuilder();
            builder.setPlayerid(playerId);
            builder.setPlayeridcollide(playerIdCollide);
            builder.setPropid(propId);
            builder.setTemplateid(templateId);
            Game.sPropCollidePlayerPush push = builder.build();

            Match match = GetMatch(matchId);
            if (match == null) {
                Log4j2.LOGGER.warn("[Proto:] matchId in cProtoPropCollisionReq cannot be found in match, id = " + matchId);
                return;
            }

            if (match.Contains(nd.GetClientId())) {
                match.SendMessageToAllClients(MsgTypeOuterClass.MsgType.sPropCollidePlayerPush.getNumber(), push.toByteArray());
            }
        }
        catch (Exception e) {
            Log4j2.LOGGER.error(e.toString());
        }

    }

    private void ProtoPropCollidePropReq(NetData nd) {
        if (!Util.CheckLogon(nd)) return;

        try {
            Game.cPropCollidePropReq req = Game.cPropCollidePropReq.parseFrom(nd.GetData());
            int matchid = req.getMatchid();
            int playerId = req.getPlayerid();
            int propId = req.getPropid();
            int templateId = req.getTemplateid();
            int propIdCollide = req.getPropidcollide();
            int playerIdCollide = req.getPlayeridcollide();
            int templateIdCollide = req.getTemplateidcollide();

            Match match = GetMatch(matchid);

            if (match.Contains(nd.GetClientId())) {
                Game.sPropCollidePropPush.Builder builder = Game.sPropCollidePropPush.newBuilder();
                builder.setPlayerid(playerId);
                builder.setPlayeridcollide(playerIdCollide);
                builder.setTemplateid(templateId);
                builder.setTemplateidcollide(templateIdCollide);
                builder.setPropid(propId);
                builder.setPropidcollide(propIdCollide);
                Game.sPropCollidePropPush push = builder.build();
                match.SendMessageToAllClients(MsgTypeOuterClass.MsgType.sPropCollidePropPush.getNumber(), push.toByteArray());
            }
        }
        catch (Exception e) {
            Log4j2.LOGGER.error(e.toString());
        }
    }

    private void ProtoPropLifeEndReq(NetData nd) {
        try {
            Game.cPropLifeEndReq req = Game.cPropLifeEndReq.parseFrom(nd.GetData());
            int matchId = req.getMatchid();
            int templateId = req.getTemplateid();
            int propId = req.getPropid();
            int clientId = req.getPlayerid();

            Game.sPropRemovePush.Builder builder = Game.sPropRemovePush.newBuilder();
            builder.setTemplateid(templateId);
            builder.setId(propId);
            builder.setPlayerid(clientId);
            Game.sPropRemovePush push = builder.build();

            Match match = GetMatch(matchId);
            if (match == null) {
                Log4j2.LOGGER.warn("[Proto:] matchId in cProtoPropLifeEndReq cannot be found in match, id = " + matchId);
                return;
            }

            if (match.Contains(nd.GetClientId())) {
                match.SendMessageToAllClients(MsgTypeOuterClass.MsgType.sPropRemovePush.getNumber(), push.toByteArray());
            }
        } catch (Exception e) {
            Log4j2.LOGGER.error(e.toString());
        }

    }

    private void ProtoSyncAirPlaneTransformReq(NetData nd) {
        if (!Util.CheckLogon(nd)) return;

        try {
            Game.cSyncAirPlaneTransformReq req = Game.cSyncAirPlaneTransformReq.parseFrom(nd.GetData());
            int matchid = req.getMatchid();
            Match match = GetMatch(matchid);
            if (match == null) {
                //TODO:已经关闭match后收到客户端的信息的话就需要告知问题，这里应该用错误码
                return;
            }

            if (match.Contains(nd.GetClientId())) {
                match.SyncAirPlaneTransform(nd.GetClientId(), req.getX(), req.getY(), req.getAngle());
            }

        } catch (Exception e) {
            Log4j2.LOGGER.error(e.toString());
        }
    }

    private void ProtoScoreSyncReq(NetData nd) {
        if (!Util.CheckLogon(nd)) return;

        try {
            Game.cMatchScoreSyncReq req = Game.cMatchScoreSyncReq.parseFrom(nd.GetData());
            int score = req.getScore();
            int uid = req.getClientid();
            int playerid = req.getPlayerid();
            int matchid = req.getMatchid();

            //写入数据库
            MongoCollection<Document> collection = DatabaseControl.Instance().getDatabase().getCollection("basic");
            Document doc = collection.find(eq("UserId", uid)).first();
            if (doc == null) {
                // 为该用户生成token
                try {
                    Document document = new Document("UserId", uid)
                            .append("LastMatchScore", score)
                            .append("TotalScore", score);
                    collection.insertOne(document);
                } catch(Exception exception) {
                    Log4j2.LOGGER.error(exception.toString());
                }
            }
            else {
                BsonDocument bsonDoc = doc.toBsonDocument(BsonDocument.class, MongoClient.getDefaultCodecRegistry());
                BsonValue value = bsonDoc.get("TotalScore");
                BsonInt32 b32 = value.asInt32();
                int totalScore = b32.getValue() + score;

                Bson bson = Filters.eq("UserId", uid);
                collection.updateOne(bson, Updates.set("LastMatchScore", score));
                collection.updateOne(bson, Updates.set("TotalScore", totalScore));
            }

            //同步到其他
            Match match = GetMatch(matchid);
            if (match == null) {
                Log4j2.LOGGER.error("[Proto:] match id in ProtoScoreSyncReq cannot be found, id = " + matchid);
            }
            else if(match.Contains(playerid)) {
                Game.sMatchScoreSyncPush.Builder builder = Game.sMatchScoreSyncPush.newBuilder();
                builder.setPlayerid(playerid);
                builder.setScore(score);

                Game.sMatchScoreSyncPush push = builder.build();
                match.SendMessageToClientsExcept(nd.GetClientId(), MsgTypeOuterClass.MsgType.sMatchScoreSyncPush.getNumber(), push.toByteArray());
            }
        }catch (Exception e) {
            Log4j2.LOGGER.error(e.toString());
        }
    }

    private void ProtoSyncPropTransformReq(NetData nd) {
        if (!Util.CheckLogon(nd)) return;

        try {
            Game.cSyncPropTransformReq req = Game.cSyncPropTransformReq.parseFrom(nd.GetData());
            int propId = req.getPropid();
            int templateId = req.getTemplateid();
            float x = req.getX();
            float y = req.getY();
            float angle = req.getAngle();
            int matchId = req.getMatchid();

            Game.sSyncPropTransformPush.Builder builder = Game.sSyncPropTransformPush.newBuilder();
            builder.setTemplateid(templateId);
            builder.setPropid(propId);
            builder.setX(x);
            builder.setY(y);
            builder.setAngle(angle);
            Game.sSyncPropTransformPush push = builder.build();

            Match match = GetMatch(matchId);
            if (match == null) {
                Log4j2.LOGGER.warn("[Proto:] matchId in cProtoSyncPropTransformReq cannot be found: id = " + matchId);
                return;
            }

            if (match.Contains(nd.GetClientId())) {
                match.SendMessageToClientsExcept(nd.GetClientId(), MsgTypeOuterClass.MsgType.sSyncPropTransformPush.getNumber(), push.toByteArray());
            }
        }catch (Exception e) {
            Log4j2.LOGGER.error(e.toString());
        }
    }

    private void ProtoCountrySeletionReq(NetData nd) {
        if (!Util.CheckLogon(nd)) return;

        try {
            Game.cCountrySelectionReq req = Game.cCountrySelectionReq.parseFrom(nd.GetData());
            int matchid = req.getMatchid();
            int playerid = req.getPlayerid();
            String country = req.getCountry();

            Match match = GetMatch(matchid);
            if (match == null) {
                Log4j2.LOGGER.warn("[Proto:] matchId in cCountrySelectionReq cannot be found in match, id = " + matchid);
                return;
            }

            if (match.Contains(nd.GetClientId())) {
                Game.sCountrySelectionPush.Builder builder = Game.sCountrySelectionPush.newBuilder();
                builder.setPlayerid(playerid);
                builder.setCountry(country);
                Game.sCountrySelectionPush push = builder.build();
                match.SendMessageToClientsExcept(nd.GetClientId(), MsgTypeOuterClass.MsgType.sCountrySelectionPush.getNumber(), push.toByteArray());
            }

        }catch (Exception e) {
            Log4j2.LOGGER.error(e.toString());
        }
    }

    private void ProtoAirPlaneLifeEndReq(NetData nd) {
        if (!Util.CheckLogon(nd)) return;

        try {
            Game.cAirPlaneLifeEndReq req = Game.cAirPlaneLifeEndReq.parseFrom(nd.GetData());

            Match match = GetMatch(req.getMatchid());
            if(match.Contains(req.getPlayerid())) {
                match.ProcessPlayerLifeEnd(req.getPlayerid());
            }
        }
        catch (Exception e) {
            Log4j2.LOGGER.error(e.toString());
        }
    }


    private void ProtoPropSpawnReq(NetData nd) {
        if (!Util.CheckLogon(nd)) return;

        try {
            Game.cPropSpawnReq req = Game.cPropSpawnReq.parseFrom(nd.GetData());

            Match match = GetMatch(req.getMatchid());
            if (match == null) {
                Log4j2.LOGGER.warn("[Proto:] matchId in cPropSpawnReq cannot be found in match, id = " + req.getMatchid());
                return;
            }
            if (match.Contains(nd.GetClientId())) {
                match.GetPropManager().CreateProp(req.getPlayerid(), req.getTemplateid(), req.getX(), req.getY(), req.getAngle());
            }
        }
        catch (Exception e) {
            Log4j2.LOGGER.error(e.toString());
        }
    }

    private void ProtoPlayerCollideBoundReq(NetData nd) {
        try {
            Game.cPlayerCollideBoundReq req = Game.cPlayerCollideBoundReq.parseFrom(nd.GetData());
            int matchid = req.getMatchid();
            int playerid = req.getPlayerid();

            Match match = GetMatch(matchid);
            if (match != null) {

                if (match.Contains(playerid)) {
                    Game.sPlayerCollideBoundPush.Builder builder = Game.sPlayerCollideBoundPush.newBuilder();
                    builder.setPlayerid(playerid);
                    Game.sPlayerCollideBoundPush push = builder.build();
                    match.SendMessageToAllClients(MsgTypeOuterClass.MsgType.sPlayerCollideBoundPush.getNumber(), push.toByteArray());
                }
            }
        }
        catch (Exception e) {
            Log4j2.LOGGER.error(e.toString());
        }
    }

    private void ProtoPlayerCollidePlayerReq(NetData nd) {
        try {
            Game.cPlayerCollidePlayerReq req = Game.cPlayerCollidePlayerReq.parseFrom(nd.GetData());
            int matchid = req.getMatchid();
            int playerid = req.getPlayerid();
            int playercollide = req.getPlayeridcollide();

            Match match = GetMatch(matchid);
            if (match != null) {
                if (match.Contains(playerid) && match.Contains(playercollide)) {
                    Game.sPlayerCollidePlayerPush.Builder builder = Game.sPlayerCollidePlayerPush.newBuilder();
                    builder.setPlayerid(playerid);
                    builder.setPlayeridcollide(playercollide);
                    Game.sPlayerCollidePlayerPush push = builder.build();
                    match.SendMessageToAllClients(MsgTypeOuterClass.MsgType.sPlayerCollidePlayerPush.getNumber(), push.toByteArray());
                }
            }
        }
        catch (Exception e) {
            Log4j2.LOGGER.error(e.toString());
        }
    }

    private void ProtoPropCollideBoundReq(NetData nd) {
        try {
            Game.cPropCollideBoundReq req = Game.cPropCollideBoundReq.parseFrom(nd.GetData());
            int matchid = req.getMatchid();
            int templateid = req.getTemplateid();
            int propid = req.getPropid();

            Match match = GetMatch(matchid);
            if (match != null) {
                Game.sPropCollideBoundPush.Builder builder = Game.sPropCollideBoundPush.newBuilder();
                builder.setPropid(propid);
                builder.setTemplateid(templateid);
                Game.sPropCollideBoundPush push = builder.build();
                match.SendMessageToAllClients(MsgTypeOuterClass.MsgType.sPlayerCollideBoundPush.getNumber(), push.toByteArray());
            }

        }
        catch (Exception e) {
            Log4j2.LOGGER.error(e.toString());
        }
    }

    private void ProtoHeartBeatReq(NetData nd) {
        String clientId = nd.GetClientId();

        for (Map.Entry<Integer, Match> entry : matchMap.entrySet()) {
            if (entry.getValue().Contains(clientId)) {
                entry.getValue().ResetClientHeartBeatTimer(clientId);
            }
        }
    }

    public void RegisterProtoProcessor() {
        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cStartMatchReq.getNumber(), this, "ProtoStartMatchReq");
        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cCancelMatchReq.getNumber(), this, "ProtoCancelMatchReq");
        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cPropStubCollisionReq.getNumber(), this, "ProtoPropStubCollisionReq");
        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cPropCollidePlayerReq.getNumber(), this, "ProtoPropCollidePlayerReq");
        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cPropCollidePropReq.getNumber(), this, "ProtoPropCollidePropReq");

        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cPlayerCollideBoundReq.getNumber(), this, "ProtoPlayerCollideBoundReq");
        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cPlayerCollidePlayerReq.getNumber(), this, "ProtoPlayerCollidePlayerReq");
        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cPropCollideBoundReq.getNumber(), this, "ProtoPropCollideBoundReq");

        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cPropLifeEndReq.getNumber(), this, "ProtoPropLifeEndReq");
        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cMatchScoreSyncReq.getNumber(), this, "ProtoScoreSyncReq");
        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cSyncAirPlaneTransformReq.getNumber(), this, "ProtoSyncAirPlaneTransformReq");
        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cSyncPropTransformReq.getNumber(), this, "ProtoSyncPropTransformReq");
        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cCountrySelectionReq.getNumber(), this, "ProtoCountrySeletionReq");
        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cAirPlaneLifeEndReq.getNumber(), this, "ProtoAirPlaneLifeEndReq");
        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cPropSpawnReq.getNumber(), this, "ProtoPropSpawnReq");
        ProtoCenter.RegisterProtoProcessor(MsgTypeOuterClass.MsgType.cHeartBeatReq.getNumber(), this, "ProtoHeartBeatReq");

    }
}
